/**
 * Created by yang on 2015/8/25.
 */
'use strict';

var fs = require('fs');
var net = require('net');
var is = require('is-type-of');
var Promise = require('bluebird');
var logger = require('./logger.js');
var protocol = require('./protocol.js');
var helpers = require('./helpers.js');

module.exports = Storage;

function Storage(config) {
    this.config = config;
    this._name = config.host + ':' + config.port;
}

// private methods

Storage.prototype._getConnection = function() {
    return this._newConnection();
};

Storage.prototype._newConnection = function() {
    var _self = this;
    return new Promise(function(resolve, reject) {
        var socket = new net.Socket();
        logger.debug('connect to storage server [%s].', _self._name);
        socket.setTimeout(_self.config.timeout);
        socket.connect(_self.config.port, _self.config.host);

        socket.on('error', function(err) {
            reject(err);
        });

        socket.on('timeout', function() {
            socket.destroy();
            reject(new Error('connect to storage server [' + _self._name + '] timeout.'));
        });

        socket.on('connect', function() {
            logger.debug('storage server [%s] is connected', _self._name);
            resolve(socket);
        });
    });

};

/**
 * # request body:
 @ 1 byte: store path index on the storage server
 @ FDFS_PROTO_PKG_LEN_SIZE bytes: file size
 @ FDFS_FILE_EXT_NAME_MAX_LEN bytes: file ext name, do not include dot (.)
 @ file size bytes: file content

 # response body:
 @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
 @ filename bytes: filename
 * @param command
 * @param file
 * @param options
 */
Storage.prototype._uploadFile = function(command, file, options) {
    var _self = this;
    return this._getConnection().then(function(socket) {
        logger.debug('start upload file to storage server [%s]', _self._name);
        // 封装header并发送
        var bodyLength = 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_FILE_EXT_NAME_MAX_LEN + options.size;
        var header = protocol.packHeader(command, bodyLength, 0);
        socket.write(header);

        // 封装并发送body
        // 除file content外的内容
        var buffer = new Buffer(1 + protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_FILE_EXT_NAME_MAX_LEN);
        buffer.fill(0);
        buffer.writeUInt8(_self.config.storePathIndex, 0);
        helpers.number2Buffer(options.size, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(buffer, 1);
        var extBL = Buffer.byteLength(options.ext, _self.config.charset);
        buffer.write(options.ext, 1 + protocol.FDFS_PROTO_PKG_LEN_SIZE, extBL, _self.config.charset);

        socket.write(buffer);

        // 发送file content
        if (is.string(file)) {
            file = fs.createReadStream(file);
        }

        // buffer
        if (is.buffer(file)) {
            socket.write(file);

            // stream
        } else {
            file.pipe(socket, {end: false});
        }

        return new Promise(function(resolve, reject) {
            protocol.recvPacket(
                socket,
                protocol.STORAGE_PROTO_CMD_RESP,
                null,
                function(err, body) {
                    if (err) {
                        reject(err);
                        return;
                    }
                    // 校验body
                    if (body.length <= protocol.FDFS_GROUP_NAME_MAX_LEN) {
                        throw new Error('response body length: ' + body.length + ' <= ' + protocol.FDFS_GROUP_NAME_MAX_LEN);
                    }

                    var fileId = _parseFileId(body, _self.config.charset);
                    resolve(fileId);
                }
            );
        });
    });
};

/**
 * # request body:
 @ FDFS_PROTO_PKG_LEN_SIZE bytes: filename length
 @ FDFS_PROTO_PKG_LEN_SIZE bytes: file size
 @ filename bytes: filename
 @ file size bytes: file content

 # response body: none
 * @param file
 * @param options
 */
Storage.prototype._appendFile = function(file, options) {
    var _self = this;
    return this._getConnection().then(function(socket) {
        logger.debug('start upload file to storage server [%s]', _self._name);
        var gf = helpers.id2gf(options.fileId);
        // 封装header并发送
        var filenameBL = Buffer.byteLength(gf.filename, _self.config.charset);
        var bodyLength = 2*protocol.FDFS_PROTO_PKG_LEN_SIZE + filenameBL + options.size;
        var command = protocol.STORAGE_PROTO_CMD_APPEND_FILE;
        var header = protocol.packHeader(command, bodyLength, 0) ;
        socket.write(header);

        // 封装并发送body
        // 除file content外的内容
        var buffer = new Buffer(2*protocol.FDFS_PROTO_PKG_LEN_SIZE + filenameBL);
        buffer.fill(0);
        helpers.number2Buffer(gf.filename.length, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(buffer, 0);
        helpers.number2Buffer(options.size, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(buffer, protocol.FDFS_PROTO_PKG_LEN_SIZE);
        buffer.write(gf.filename, 2*protocol.FDFS_PROTO_PKG_LEN_SIZE, filenameBL, _self.config.charset);

        socket.write(buffer);

        // 发送file content
        if (is.string(file)) {
            file = fs.createReadStream(file);
        }

        // buffer
        if (is.buffer(file)) {
            socket.write(file);

            // stream
        } else {
            file.pipe(socket, {end: false});
        }

        return new Promise(function(resolve, reject) {
            protocol.recvPacket(
                socket,
                protocol.STORAGE_PROTO_CMD_RESP,
                0,
                function(err) {
                    if (err) {
                        reject(err);
                        return;
                    }
                    resolve();
                }
            );
        });
    });

};

/**
 * # request body:
 @ FDFS_PROTO_PKG_LEN_SIZE bytes: filename length
 @ FDFS_PROTO_PKG_LEN_SIZE bytes: file offset
 @ FDFS_PROTO_PKG_LEN_SIZE bytes: file size
 @ filename bytes: filename
 @ file size bytes: file content

 # response body: none

 * @param file
 * @param options
 */
Storage.prototype._modifyFile = function(file, options) {
    var _self = this;
    return this._getConnection().then(function(socket) {
        logger.debug('start upload file to storage server [%s]', _self._name);
        var gf = helpers.id2gf(options.fileId);
        // 封装header并发送
        var filenameBL = Buffer.byteLength(gf.filename, _self.config.charset);
        var bodyLength = 3*protocol.FDFS_PROTO_PKG_LEN_SIZE + filenameBL + options.size;
        var command = protocol.STORAGE_PROTO_CMD_MODIFY_FILE;
        var header = protocol.packHeader(command, bodyLength, 0);
        socket.write(header);

        // 封装并发送body
        // 除file content外的内容
        var buffer = new Buffer(3*protocol.FDFS_PROTO_PKG_LEN_SIZE + filenameBL);
        buffer.fill(0);
        helpers.number2Buffer(gf.filename.length, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(buffer, 0);
        helpers.number2Buffer(options.offset, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(buffer, protocol.FDFS_PROTO_PKG_LEN_SIZE);
        helpers.number2Buffer(options.size, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(buffer, 2*protocol.FDFS_PROTO_PKG_LEN_SIZE);
        buffer.write(gf.filename, 3*protocol.FDFS_PROTO_PKG_LEN_SIZE, filenameBL, _self.config.charset);

        socket.write(buffer);

        // 发送file content
        if (is.string(file)) {
            file = fs.createReadStream(file);
        }

        // buffer
        if (is.buffer(file)) {
            socket.write(file);

            // stream
        } else {
            file.pipe(socket, {end: false});
        }

        return new Promise(function(resolve, reject) {
            protocol.recvPacket(
                socket,
                protocol.STORAGE_PROTO_CMD_RESP,
                0,
                function(err) {
                    if (err) {
                        reject(err);
                        return;
                    }
                    resolve();
                }
            );
        });
    });

};

// upload相关

// public methods

/**
 * # request body:
     @ 1 byte: store path index on the storage server
     @ FDFS_PROTO_PKG_LEN_SIZE bytes: file size
     @ FDFS_FILE_EXT_NAME_MAX_LEN bytes: file ext name, do not include dot (.)
     @ file size bytes: file content

   # response body:
     @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
     @ filename bytes: filename
 * @param file
 * @param options
 */
Storage.prototype.upload = function(file, options) {
    if (!options.method || options.method === protocol.FDFS_METHOD_UPLOAD_FILE) {
        var command = protocol.STORAGE_PROTO_CMD_UPLOAD_FILE;
        return this._uploadFile(command, file, options);
    } else if (options.method === protocol.FDFS_METHOD_UPLOAD_APPENDER_FILE) {
        var command = protocol.STORAGE_PROTO_CMD_UPLOAD_APPENDER_FILE;
        return this._uploadFile(command, file, options);
    } else if (options.method === protocol.FDFS_METHOD_APPEND_FILE) {
        return this._appendFile(file, options);
    } else if (options.method === protocol.FDFS_METHOD_MODIFY_FILE) {
        return this._modifyFile(file, options);
    }
    return Promise.reject(new Error("options.method must in ['upload', 'uploadAppender', 'append', 'modify'] "));
};

/**
 * * STORAGE_PROTO_CMD_SET_METADATA
 *
     # function: set meta data
     # request body:
         @ FDFS_PROTO_PKG_LEN_SIZE bytes: filename length
         @ FDFS_PROTO_PKG_LEN_SIZE bytes: meta data size
         @ 1 bytes: operation flag,
             'O' for overwrite all old metadata
             'M' for merge, insert when the meta item not exist, otherwise update it
         @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
         @ filename bytes: filename
         @ meta data bytes: each meta data seperated by \x01,
         name and value seperated by \x02
     # response body: none
 * @param fileId
 * @param metaData
 */
Storage.prototype.setMetaData = function(fileId, metaData, flag) {
    flag = flag || protocol.STORAGE_SET_METADATA_FLAG_OVERWRITE;

    var _self = this;
    return this._getConnection().then(function(socket) {
        var gf = helpers.id2gf(fileId);
        var packedMeta = protocol.packMetaData(metaData);

        // 封装header
        var charset = _self.config.charset;
        var command = protocol.STORAGE_PROTO_CMD_SET_METADATA;
        var fnLength = Buffer.byteLength(gf.filename, charset);
        var metaLength = Buffer.byteLength(packedMeta, charset);
        var bodyLength = protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE + 1 +
            protocol.FDFS_GROUP_NAME_MAX_LEN + fnLength + metaLength;

        var header = protocol.packHeader(command, bodyLength, 0);
        socket.write(header);

        // 封装body
        var groupLength = Buffer.byteLength(gf.group, charset);
        var body = new Buffer(bodyLength);
        body.fill(0);
        helpers.number2Buffer(fnLength, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(body, 0);
        helpers.number2Buffer(metaLength, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(body, protocol.FDFS_PROTO_PKG_LEN_SIZE);
        body.write(flag, protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE, 1, charset);
        body.write(gf.group, protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE + 1, groupLength, charset);
        body.write(gf.filename, protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE + 1 + protocol.FDFS_GROUP_NAME_MAX_LEN, fnLength, charset);
        body.write(packedMeta, bodyLength - metaLength, metaLength, charset);

        socket.write(body);

        return new Promise(function(resolve, reject) {
            protocol.recvPacket(
                socket,
                protocol.STORAGE_PROTO_CMD_RESP,
                0,
                function(err) {
                    if (err) {
                        reject(err);
                        return;
                    }
                    resolve();
                }
            );
        });
    });

};

/**
 * * STORAGE_PROTO_CMD_GET_METADATA
     # function: get metat data from storage server
     # request body:
     @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
     @ filename bytes: filename
     # response body
     @ meta data buff, each meta data seperated by \x01, name and value seperated by \x02
 * @param fileId
 */
Storage.prototype.getMetaData = function(fileId) {
    var _self = this;
    return this._getConnection().then(function(socket) {
        var gf = helpers.id2gf(fileId);
        var packet = protocol.packFileId(protocol.STORAGE_PROTO_CMD_GET_METADATA, gf.group, gf.filename, _self.config.charset);
        socket.write(packet);

        return new Promise(function(resolve, reject) {
            protocol.recvPacket(
                socket,
                protocol.STORAGE_PROTO_CMD_RESP,
                null,
                function(err, body) {
                    if (err) {
                        reject(err);
                        return;
                    }
                    var rawMeta = body.toString(_self.config.charset);
                    if (rawMeta) {
                        var metaData = protocol.parseMetaData(rawMeta);
                        resolve(metaData);
                    } else {
                        resolve(rawMeta);
                    }
                }
            );
        });
    });
};

/**
 * 删除文件
 * STORAGE_PROTO_CMD_DELETE_FILE
 * # request body:
     @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
     @ filename bytes: filename

   # response body: none
 * @param fileId
 */
Storage.prototype.del = function(fileId) {
    var _self = this;
    return this._getConnection().then(function(socket) {
        var gf = helpers.id2gf(fileId);
        var packet = protocol.packFileId(protocol.STORAGE_PROTO_CMD_DELETE_FILE, gf.group, gf.filename, _self.config.charset);
        socket.write(packet);

        return new Promise(function(resolve, reject) {
            protocol.recvPacket(
                socket,
                protocol.STORAGE_PROTO_CMD_RESP,
                0,
                function(err) {
                    if (err) {
                        reject(err);
                        return;
                    }
                    resolve();
                }
            );
        });
    });
};

/**
 * STORAGE_PROTO_CMD_DOWNLOAD_FILE
     # function: download/fetch file from storage server
     # request body:
         @ FDFS_PROTO_PKG_LEN_SIZE bytes: file offset
         @ FDFS_PROTO_PKG_LEN_SIZE bytes: download file bytes
         @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
         @ filename bytes: filename
 
     # response body:
        @ file content
 * @param fileId
 * @param options
 *      options.target 下载的文件流将被写入到这里，可以是本地文件名，也可以是WritableStream
 *      options.offset和options.bytes: 当只想下载文件中的某1片段时指定
 */
Storage.prototype.download = function(fileId, options) {
    var _self = this;
    return this._getConnection().then(function(socket) {
        var gf = helpers.id2gf(fileId);
        var charset = _self.config.charset;
        // 封装header
        var fnLength = Buffer.byteLength(gf.filename, charset);
        var bodyLength = protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_GROUP_NAME_MAX_LEN + fnLength;
        var header = protocol.packHeader(protocol.STORAGE_PROTO_CMD_DOWNLOAD_FILE, bodyLength, 0);

        // 封装body
        var body = new Buffer(bodyLength);
        // 默认都填充上0
        body.fill(0);
        if (options.offset) {
            helpers.number2Buffer(options.offset, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(body);
        }
        if (options.bytes) {
            helpers.number2Buffer(options.bytes, protocol.FDFS_PROTO_PKG_LEN_SIZE).copy(body, protocol.FDFS_PROTO_PKG_LEN_SIZE);
        }
        var groupBL = Buffer.byteLength(gf.group, charset);
        body.write(gf.group, protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE, groupBL, charset);
        body.write(gf.filename, protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_PROTO_PKG_LEN_SIZE + protocol.FDFS_GROUP_NAME_MAX_LEN, fnLength, charset);

        socket.write(Buffer.concat([header, body]));

        return new Promise(function(resolve, reject) {
            var header;
            var target = options.target;
            // 已接收的body length
            var recvLength = 0;

            protocol.recvPacket(
                socket,
                protocol.STORAGE_PROTO_CMD_RESP,
                null,
                function(err, data) {
                    if (err) {
                        reject(err);
                        return;
                    }
                    // header
                    if (!is.buffer(data)) {
                        header = data;

                        // body
                    } else {
                        if (!target) {
                            resolve({
                                data: data,
                                bodyLength: header.bodyLength
                            });
                        } else {
                            target.write(data);
                        }

                        recvLength += data.length;
                        // 读取完毕
                        if (recvLength >= header.bodyLength) {
                            // 读取完毕要关闭文件。
                            // 不关闭文件造成两种后果：1、文件句柄不释放，造成硬盘空间不释放。
                            // 2、在文件还没有写完时候就返回文件,表现如express中返回图片只有一半。
                            target.on('close', function () {
                                protocol.closeSocket(socket);
                                resolve();
                            });
                            target.on('finish', function () {
                                protocol.closeSocket(socket);
                                resolve();
                            })
                            target.end();
                        }
                    }
                },
                true
            );
        });
    });
};

/**
 * * STORAGE_PROTO_CMD_QUERY_FILE_INFO
     # function: query file info from storage server
     # request body:
        @ FDFS_GROUP_NAME_MAX_LEN bytes: group name
        @ filename bytes: filename

     # response body:
        @ FDFS_PROTO_PKG_LEN_SIZE bytes: file size
        @ FDFS_PROTO_PKG_LEN_SIZE bytes: file create timestamp
        @ FDFS_PROTO_PKG_LEN_SIZE bytes: file CRC32 signature
        @ FDFS_IPADDR_SIZE bytes: file source ip addr
 * @param fileId
 */
Storage.prototype.getFileInfo = function(fileId) {
    var charset = this.config.charset;
    return this._getConnection().then(function(socket) {
        var gf = helpers.id2gf(fileId);
        var packet = protocol.packFileId(protocol.STORAGE_PROTO_CMD_QUERY_FILE_INFO, gf.group, gf.filename, charset);
        socket.write(packet);

        return new Promise(function(resolve, reject) {
            protocol.recvPacket(
                socket,
                protocol.STORAGE_PROTO_CMD_RESP,
                protocol.FDFS_PROTO_PKG_LEN_SIZE * 3 + protocol.FDFS_IPADDR_SIZE,
                function(err, body) {
                    if (err) {
                        reject(err);
                        return;
                    }
                    var result = {
                        size: parseInt(body.toString('hex', 0, protocol.FDFS_PROTO_PKG_LEN_SIZE), 16),
                        timestamp: helpers.buffer2Number(body, protocol.FDFS_PROTO_PKG_LEN_SIZE),
                        crc32: helpers.buffer2Number(body, protocol.FDFS_PROTO_PKG_LEN_SIZE * 2),
                        addr: helpers.trim(body.toString(charset, protocol.FDFS_PROTO_PKG_LEN_SIZE * 3))
                    };
                    resolve(result);
                }
            );
        });
    });
};

// helpers
/**
 * parse file id from body
 * @param body
 * @param charset
 */
function _parseFileId(body, charset) {
    var group = helpers.trim(body.toString(charset, 0, protocol.FDFS_GROUP_NAME_MAX_LEN));
    var filename = helpers.trim(body.toString(charset, protocol.FDFS_GROUP_NAME_MAX_LEN));
    return group + '/' + filename;
}
